package com.nw.dsl4j.generator;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.antlr.Tool;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;

import com.nw.dsl4j.annotations.DSL;
import com.nw.dsl4j.annotations.EmbeddedRule;
import com.nw.dsl4j.annotations.Flag;
import com.nw.dsl4j.annotations.Map;
import com.nw.dsl4j.annotations.Resolve;
import com.nw.dsl4j.annotations.Rule;
import com.nw.dsl4j.annotations.SubtypeAlternatives;
import com.nw.dsl4j.annotations.Token;
import com.nw.dsl4j.annotations.TokenSeperatedCollection;
import com.nw.dsl4j.generator.model.AntlrRule;
import com.nw.dsl4j.generator.model.Grammar;

public class Generator {
	private final TemplateEngine templateEngine = new TemplateEngine(this);

	private final Grammar grammer = new Grammar();

	private final List<String> imports = new ArrayList<String>();

	private final String javaTarget;

	private final Class<?> dslClass;
	
	private final Set<Class<?>> processed = new HashSet<Class<?>>();

	private final String grammerTarget;

	public Generator(String className, String javaTarget, String grammerTarget, ClassLoader classLoader) throws Exception {
		this.javaTarget = javaTarget;
		this.grammerTarget = grammerTarget;
		this.dslClass = classLoader.loadClass(className);
	}

	public void generate() throws IOException{

		generateGrammer();

		grammer.setPackageName(dslClass.getPackage().getName() + ".parser");

		String grammerFileContent = getTemplateEngine().processTemplateByName("grammar.vm", new ContextVar("doCode", true),
				new ContextVar("grammer", grammer));

		String grammerFileName = grammer.getName() + ".g";

		File file = writeFile(grammerFileName, grammerFileContent);

		String genFolder = javaTarget + "/" + grammer.getPackageName().replace(".", "/");
		Tool antlr = new Tool(new String[] { "-o", genFolder, file.getAbsolutePath() });
		antlr.process();
	}

	@SuppressWarnings("unchecked")
	public Grammar generateGrammer() {
		grammer.setName(dslClass.getSimpleName());

		DSL dslAnnotation = dslClass.getAnnotation(DSL.class);

		grammer.setForwardLooking(dslAnnotation.forwardLooking());
		
		for (String imp : dslAnnotation.imports()) {
			imports.add(imp);
		}
		imports.add(dslClass.getPackage().getName());

		processClass(dslClass);

		return grammer;
	}

	@SuppressWarnings("unchecked")
	void processClass(Class cls) {
		if (processed.contains(cls)) {
			return;
		} else {
			processed.add(cls);
		}
		if (DsljReflectionUtils.isToken(cls)) {
			processTokenClass(cls);
		} else if (DsljReflectionUtils.isRule(cls)) {
			if (DsljReflectionUtils.isSubTypeAlternative(cls)) {
				processSubTypeAlternatives(cls);
			} else {
				processRuleClass(cls);
			}
		} else {
			// TOOD: warn
//			throw new IllegalStateException(cls.getCanonicalName() + " is not a @Rule or @Token class");
		}
	}

	@SuppressWarnings("unchecked")
	private void processTokenClass(Class ruleCls) {

		AntlrRule antlrRule = new AntlrRule();
		antlrRule.setName(DsljReflectionUtils.getAntlrRuleName(ruleCls));

		Token token = (Token) ruleCls.getAnnotation(Token.class);
		antlrRule.setPattern(token.pattern());

		if (token.keyword()) {
			grammer.getKeywords().add(antlrRule);
		} else {
			grammer.getTokens().add(antlrRule);
		}
	}

	private void processSubTypeAlternatives(Class<?> cls) {
		
		AntlrRule antlrRule = new AntlrRule();
		antlrRule.setName(DsljReflectionUtils.getAntlrRuleName(cls));
		antlrRule.setReturns(cls);
		antlrRule.setPattern(getSubclassAlternativesPattern(cls));		

		grammer.getRules().add(antlrRule);
	}

	@SuppressWarnings("unchecked")
	private String getSubclassAlternativesPattern(Class cls) {
		SubtypeAlternatives subtypeAlternatives = (SubtypeAlternatives) cls.getAnnotation(SubtypeAlternatives.class);
		List<String> subtypesNames = new ArrayList<String>();
		for (Class c : subtypeAlternatives.alternatives()) {
			List<ContextVar> vars = new ArrayList<ContextVar>();
			vars.add(new ContextVar("alias", DsljReflectionUtils.getAntlrRuleName(c)));
			vars.add(new ContextVar("rulename", DsljReflectionUtils.getAntlrRuleName(c)));
			vars.add(new ContextVar("doCode", true));
			vars.add(new ContextVar("ruleclass", c.getCanonicalName()));
			Rule rule = (Rule) c.getAnnotation(Rule.class);
			if (!rule.hint().equals("")) {
				vars.add(new ContextVar("doHint", true));
				vars.add(new ContextVar("hint", getRuleHintPattern(c)));
			} else {
				vars.add(new ContextVar("doHint", false));
			}
			String pattern = getTemplateEngine().processTemplateByName("alternative.vm", vars.toArray(new ContextVar[0])); 

			subtypesNames.add(pattern);
			processClass(c);
		}
		return StringUtils.join(subtypesNames.toArray(), "|");
	}

	@SuppressWarnings("unchecked")
	private void processRuleClass(Class cls) {

		AntlrRule antlrRule = new AntlrRule();
		antlrRule.setName(DsljReflectionUtils.getAntlrRuleName(cls));
		antlrRule.setReturns(cls);
		antlrRule.setPattern(getRulePattern(cls));

		grammer.getRules().add(antlrRule);
	}

	@SuppressWarnings("unchecked")
	private String getRulePattern(Class ruleClass) {
		Rule rule = (Rule) ruleClass.getAnnotation(Rule.class);
		List<ContextVar> vars = getClassContextVars(ruleClass, true, true);
		return getTemplateEngine().processTemplate(rule.pattern(), vars);
	}

	@SuppressWarnings("unchecked")
	private String getRuleHintPattern(Class ruleClass) {
		Rule rule= (Rule) ruleClass.getAnnotation(Rule.class);
		List<ContextVar> vars = getClassContextVars(ruleClass, false, false);
		vars.add(new ContextVar("doCode", false));
		return getTemplateEngine().processTemplate(rule.hint(), vars);
	}

	private List<ContextVar> getClassContextVars(Class<?> ruleClass, boolean doCode, boolean doHinting) {
		List<ContextVar> vars = new ArrayList<ContextVar>();

		for (Field f : DsljReflectionUtils.getAllRuleRelatedFields(ruleClass)) {
			FieldPattern fieldPattern = new FieldPattern();
			fieldPattern.setPattern(getFieldPattern(f, doCode, doHinting, false));
			fieldPattern.setSingular(getFieldPattern(f, doCode, doHinting, true));
			vars.add(new ContextVar(f.getName(), fieldPattern));
		}
		return vars;
	}

	@SuppressWarnings("unchecked")
	private String getFieldPattern(Field f, boolean doCode, boolean doHinting, boolean singular) {
		List<ContextVar> vars = new ArrayList<ContextVar>();

		FieldInfo fieldInfo = new FieldInfo();
		
		vars.add(new ContextVar("fieldInfo", fieldInfo));
		vars.add(new ContextVar("doCode", doCode));


		fieldInfo.setCardinality(DsljReflectionUtils.getCardinality(f).toString());

		switch (DsljReflectionUtils.getCardinality(f)) {
		case One:
		case ZeroOrOne:
			fieldInfo.setSetter(RefllectionUtils.getSetterName(f));
			break;
		case OneOrMore: 
		case ZeroOrMore: 
			if (RefllectionUtils.isMap(f)) {
				fieldInfo.setMapGetter(RefllectionUtils.getGetterName(f));
				fieldInfo.setMapKeyField(f.getAnnotation(Map.class).key());
				fieldInfo.setMapKeyClass(RefllectionUtils.getMapKeyType(f));
			} else {
				fieldInfo.setCollectionGetter(RefllectionUtils.getGetterName(f));
			}
			if (f.isAnnotationPresent(TokenSeperatedCollection.class)) {
				processClass(f.getAnnotation(TokenSeperatedCollection.class).token());
				fieldInfo.setSeperatorClass(f.getAnnotation(TokenSeperatedCollection.class).token());
			}
			break;
		}
		Class<?> fieldType = RefllectionUtils.getType(f);

		if (DsljReflectionUtils.classHasParentField(fieldType)) {
			fieldInfo.setParentSetter(RefllectionUtils.getSetterName(DsljReflectionUtils.getClassParentField(fieldType)));
		}

		fieldInfo.setAlias(f.getName() + "_");
		fieldInfo.setFieldName(f.getName());
		if (singular) {
			fieldInfo.setSingular(true);
		}
		fieldInfo.setFieldClass(fieldType);

		if (doHinting) {
			Class<?> hintedClass = null;
			if(f.isAnnotationPresent(EmbeddedRule.class)) {
				EmbeddedRule embeddedRule = f.getAnnotation(EmbeddedRule.class);
				hintedClass = embeddedRule.rule();
			} else {
				hintedClass = fieldType;
			}
			
			if (hintedClass.isAnnotationPresent(Rule.class)) {
				Rule rule= hintedClass.getAnnotation(Rule.class);
				if (!rule.hint().equals("")) {
					fieldInfo.setHint(getRuleHintPattern(hintedClass));
				}
			}
		}
		
		fieldInfo.setPatternClass(fieldType);
		fieldInfo.setUseConverter(false);
		
		if (f.isAnnotationPresent(EmbeddedRule.class)) {
			EmbeddedRule embeddedToken = f.getAnnotation(EmbeddedRule.class);
			processClass(embeddedToken.rule());
			fieldInfo.setPatternClass(embeddedToken.rule());
			fieldInfo.setUseConverter(true);
			fieldInfo.setConverterName(embeddedToken.converter().getCanonicalName());
		} else if (f.isAnnotationPresent(Flag.class)) {
			Flag flag = f.getAnnotation(Flag.class);
			processClass(flag.keyword());
			fieldInfo.setFlag(true);
			fieldInfo.setPatternClass(flag.keyword());
		} else {
			processClass(fieldType);
		}
		
		if (f.isAnnotationPresent(Resolve.class)) {
			fieldInfo.setResolution(true);
		}
		
		fieldInfo.setToken(fieldInfo.getPatternClass().isAnnotationPresent(Token.class));
		fieldInfo.setRule(fieldInfo.getPatternClass().isAnnotationPresent(Rule.class));
		
		String pattern = getTemplateEngine().processTemplateByName("field.vm", vars.toArray(new ContextVar[0]));
		return pattern + "\n";
	}

	private TemplateEngine getTemplateEngine() {
		return templateEngine;
	}

	List<String> getImports() {
		return imports;
	}

	private File writeFile(String grammerFileName, String grammerFileContent) throws IOException {
		File file = new File(grammerTarget);
		file.mkdirs();
		file = new File(file, grammerFileName);
		FileUtils.writeStringToFile(file, grammerFileContent, "UTF8");
		return file;
	}
}
